package org.xx.armory.spring5.wxwork;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.spring5.mvc.IllegalRequestParamException;

import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;
import static org.xx.armory.spring5.mvc.WebUtils.getIntegerAttribute;
import static org.xx.armory.spring5.security.WebSecurityConfig.ROLE_ADMIN;
import static org.xx.armory.spring5.security.WebSecurityConfig.ROLE_BASIC;

/**
 * 提供了使用企业微信进行登录的基本功能。
 *
 * <p>{@link #setUpSignInHtml}用于显示用于扫码登录的二维码。对应的页面参考代码如下：</p>
 * <pre>
 *    window.WwLogin({
 *         "id" : "qrcode",
 *         "appid" : "[[${wwLogin.appId}]]",
 *         "agentid" : "[[${wwLogin.agentId}]]",
 *         "redirect_uri" : "[[${wwLogin.redirectUrl}]]",
 *         "state" : "[[${wwLogin.state}]]",
 *         "href" : "",
 *     });
 * </pre>
 * <p>{@link #signedIn(int, String, String, HttpSession)} 扫码登录后返回此页面。</p>
 */
@Controller
public abstract class AbstractWxWorkAuthenticationController<T> {
    private static final String WW_LOGIN_STATE_KEY = "WW_LOGIN_STATE";

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(AbstractWxWorkAuthenticationController.class);

    @Autowired
    protected WxWorkClient wxWorkClient;

    @Value("${armory.wx-work.mock-enabled:true}")
    private boolean enableMock;

    /**
     * 企业ID。每个企业微信对应的企业具有唯一ID。
     */
    @Value("${armory.wx-work.corp-id}")
    private String corpId;

    @Value("${armory.wx-work.signed-in-redirect-url}")
    private String signedInRedirectUrl;

    /**
     * 应用ID。企业内部每个应用具有唯一ID。
     */
    @Value("${armory.wx-work.agent-id}")
    private String agentId;

    protected abstract T mockPrincipal();

    @GetMapping("/mock-sign-in")
    public ModelAndView mockSignIn() {
        if (!this.enableMock) {
            throw new AccessDeniedException("Denied");
        }

        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(mockPrincipal(), null,
                                                        createAuthorityList(ROLE_BASIC, ROLE_ADMIN))
        );
        return new ModelAndView("redirect:/");
    }

    /**
     * 企业微信扫码登录页，通常需显示一个二维码。
     *
     * @param session
     *         HTTP会话。
     * @return 模型和视图。
     * @see <a href="https://work.weixin.qq.com/api/doc/90000/90135/91019">构造扫码登录链接。</a>
     */
    @GetMapping(
            value = {"/wx-work-sign-in"},
            produces = {"text/html"}
    )
    public ModelAndView setUpSignInHtml(
            HttpSession session
    ) {
        final var result = new ModelAndView();

        final var state = Math.abs(new Random().nextInt());
        session.setAttribute(WW_LOGIN_STATE_KEY, state);

        // 设置企业微信登录二维码参数
        final var wwLogin = new HashMap<String, Object>();
        wwLogin.put("appId", this.corpId);
        wwLogin.put("agentId", this.agentId);
        wwLogin.put("redirectUrl", this.signedInRedirectUrl);
        wwLogin.put("state", state);

        result.addObject("wwLogin", wwLogin);

        return result;
    }

    /**
     * 获取期望具有的帐户标签ID集合。
     *
     * @return 期望具有的帐户标签ID集合。
     * @see #tagToRoles(WxWorkTag)
     */
    protected int[] expectedTagIds() {
        return new int[0];
    }

    /**
     * 将企业微信中的帐户标签映射为对应的角色。
     *
     * @param tag
     *         帐户标签。
     * @return 对应的角色。如果没有对应的角色则返回 {@link Stream#empty} 。
     * @see #expectedTagIds()
     */
    protected Stream<String> tagToRoles(
            WxWorkTag tag
    ) {
        return Stream.empty();
    }

    protected abstract T wxWorkUserToPrincipal(
            WxWorkUser wxWorkUser
    );

    /**
     * 企业微信登录成功后的前台页面回调。
     *
     * @param state
     *         扫码登录页面设置的state参数。
     * @param authCode
     *         登录成功后，企业微信返回的授权码。
     * @param appId
     *         登录成功后，企业微信返回的应用ID。
     * @param session
     *         HTTP会话。
     * @return 模型和视图。
     */
    @GetMapping("/wx-work-signed-in")
    public ModelAndView signedIn(
            @RequestParam(value = "state") int state,
            @RequestParam(value = "code",
                          defaultValue = "") String authCode,
            @RequestParam("appid") String appId,
            HttpSession session
    ) {
        if (!appId.equalsIgnoreCase(this.corpId)) {
            throw new IllegalRequestParamException("appid", "Mismatched app-id: " + appId);
        }
        if (authCode.isEmpty()) {
            // 扫码后用户拒绝登录。
            throw new AccessDeniedException("Refused");
        }
        if (state != getIntegerAttribute(session, WW_LOGIN_STATE_KEY, -1)) {
            // state不匹配，可能是CORS攻击。
            throw new IllegalRequestParamException("state", "Mismatched state: " + state);
        }
        session.removeAttribute(WW_LOGIN_STATE_KEY);

        final var wxWorkUser = wxWorkClient.getUserByAuthCode(authCode);

        final var authorities = wxWorkClient.getUserTags(wxWorkUser, this.expectedTagIds())
                                            .stream()
                                            .flatMap(this::tagToRoles)
                                            .collect(Collectors.toSet());
        // 添加默认权限
        authorities.add(ROLE_BASIC);

        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(wxWorkUserToPrincipal(wxWorkUser), null, createAuthorityList(authorities.toArray(String[]::new)))
        );

        return new ModelAndView("redirect:/");
    }
}
